Пример организации работы с API сервера Пульт.Онлайн по протоколу JSONRPC2.0 через Websocket. Пример состоит из четырех коротких файлов (html+js+css) и не требует подключения сторонних библиотек или фреймворков.
В примере реализован минималистичный веб-сайт, отображающий список узлов одного из проектов Демо-сервера. Для каждого узла выполняется мониторинг нескольких переменных и вывод их значений на страницу, с возможностью изменять значения уставок.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Данные с сервера Пульт.Онлайн</title>
<link rel="stylesheet" href="style.css">
<script src="jsonrpc.js"></script>
<script src="script.js"></script>
</head>
<body>
<div class="container">
<h3>Данные с сервера Пульт.Онлайн</h3>
<div id="data"></div>
</div>
</body>
</html>
script.js (общая логика)
const url='wss://webscada.ru';
const apikey='31269DCE4F983A31FBE8B88DF67CDAE2';
const project='Приточная вентиляция';
const vars=['temp_setpoint','temp_outdoor','temp_room'];
let rpc=null;
let pingInterval=null;
let timeout=5000;
document.addEventListener('DOMContentLoaded',function(e){
rpc = new JsonRpc(url,timeout,onConnected,onNotification);
});
function onConnected(){
// Организуем постоянный пинг сервера, чтобы предотвратить отключение по неактивности
if(!pingInterval){
pingInterval=setInterval(function (){
rpc.call("system_ping",null,function(result, error){
console.log('pong');
});
},timeout);
}
// Получаем список всех узлов в проекте
let method='fdb_list';
let params={
pult_apikey:apikey,
path:`/projects/${project}.project/nodes`,
}
rpc.call(method,params, function(result, error){
if (error) {
console.error(method,'Error:', error);
}else{
console.log(method,'Result:', result);
// Генерируем список глобальных имен переменных
let html=[];
let global_vars=[];
for(let i=0; i<result.length; i++){
let n=result[i];
html.push(`<h4>${n.name}</h4>`);
for(let i=0; i<vars.length; i++){
let v=vars[i];
let global_name=`${n.prefix}_${v}`;
global_vars.push(global_name);
if(v==='temp_setpoint'){ // выводим поле ввода
html.push(`<div><span>${v}</span><input id='${global_name}' value='[-loading-]'/></div>`);
}else{ // выводим простой нередактируемый текст
html.push(`<div><span>${v}</span><span id='${global_name}'>[-loading-]</span></div>`);
}
}
}
// Создаем html-элементы для отображения информации
let data=document.getElementById('data');
data.innerHTML=html.join('');
// Привязывем обработчик к полям ввода
let inputs=data.querySelectorAll('input');
for(let i=0; i<inputs.length; i++){
let input=inputs[i];
input.onchange=function (e){
console.log('change',input.id,input.value);
rpc.call('var_set',{
pult_apikey:apikey,
var:input.id,
value:input.value
}, function(result, error){
console.log(result,error);
if(error){
input.classList.add('error');
setTimeout(function (){ // Развязываем алерт через таймаут, чтобы не блокировать отрисовку ошибки
alert(error.message); // Выводим ошибку
},100);
}else{
input.classList.remove('error');
input.classList.add('success');
setTimeout(function (){ // Сбрасываем класс после короткой индикации
input.classList.remove('success');
},100);
}
});
}
}
// Отправляем запрос на подписку на глобальные переменные
let params={
pult_apikey:apikey,
event:`var_update`,
options:{
'vars':global_vars,
}
}
rpc.call('subscribe',params,function (result,error){
if (error) {
console.error(method,'Error:', error);
}else{
console.log(result);
}
});
}
});
}
function onNotification(method,params){
console.log(method,params);
for(let i=0; i<params.data.changes.length; i++){
let [global_name,dt,value,error,payload]=params.data.changes[i];
let element=document.getElementById(global_name);
if(element.tagName==='INPUT'){
element.value=value;
}else{
element.innerHTML=value;
}
}
}
jsonrpc.js (библиотека для работы с JSONRPC2.0/WS)
class JsonRpc{
constructor(url,timeout,onConnected,onNotification) {
this.url = url;
this.onConnected=onConnected;
this.onNotification=onNotification;
this.socket = null;
this.callbacks = {};
this.requestId = 1;
this.timeout=timeout;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket connected');
this.onConnected();
};
this.socket.onclose = () => {
console.log('WebSocket disconnected');
// Попытка переподключения через 5 секунд
setTimeout(() => this.connect(), this.timeout);
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if(message.id===undefined){ // JSONRPC2.0 notification
this.onNotification(message.method,message.params);
}else{ // JSONRPC2.0 message
// Проверяем, что это ответ на наш запрос
if (message.id && this.callbacks[message.id]) {
const callback = this.callbacks[message.id];
delete this.callbacks[message.id];
// Вызываем коллбэк с данными ответа
callback(message.result || null, message.error || null);
}
}
} catch (e) {
console.error('Error parsing JSON-RPC message:', e);
}
};
}
call(method, params, callback) {
if (this.socket.readyState !== WebSocket.OPEN) {
console.error('WebSocket is not open');
callback(null, { code: -1, message: 'WebSocket is not open' });
return;
}
const id = this.requestId++;
const request = {
jsonrpc: "2.0",
method: method,
params: params,
id: id
};
this.callbacks[id] = callback;
this.socket.send(JSON.stringify(request));
}
}
.container{
background-color: #f0f0f0;
padding: 20px;
}
.container h4{
margin-top: 20px;
margin-bottom: 10px;
}
.error{
background-color: red;
color: white;
}
.success{
background-color: rgb(120, 245, 120);
}
#data > div{
display: table-row;
}
#data > div > *{
display: table-cell;
padding: 5px 20px 5px 5px;
}